package com.common.algorithm.aes;

import org.apache.commons.codec.digest.DigestUtils;

import javax.crypto.Cipher;
import javax.crypto.SealedObject;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;

/**
 * description: 有六种模式 <br>
 *
 * 1、ECB (Electronic Code Book)
 * 这种操作方式是所有操作方式中最简单的。明文被分成大小为128位的块。然后用相同的密钥和算法对每个块进行加密。
 * 因此，它为相同的块产生相同的结果。这是该模式的主要弱点，不建议用于加密。它需要填充数据。
 *
 * 2、CBC (Cipher Block Chaining)
 * 为了克服ECB的弱点，CBC模式使用初始化向量(IV)来增强加密。CBC首先使用明文块xor和IV，然后将结果加密为密文块。
 * 在下一个块中，它使用加密结果与明文块异或，直到最后一个块。
 * 在这种模式下，加密是不能并行化的，而解密是可以并行化的。它还需要填充数据。
 *
 * 3、CFB (Cipher FeedBack)
 * 此模式可作为流密码使用。首先对IV进行加密，然后与明文块异或以获得密文。然后CFB将加密结果加密为异或明文。需要IV.
 * 在这种模式下，解密可以并行化，而加密不能并行化。
 *
 * 4、OFB (Output FeedBack)
 * 此模式可作为流密码使用。首先对IV进行加密，然后利用加密结果对明文进行异或，得到密文。
 * 它不需要填充数据，也不会受到噪声块的影响。
 *
 * 5、CTR (Counter)
 * 这种模式使用计数器的值作为IV。它与OFB非常相似，但它每次都使用计数器来加密，而不是IV。
 * 这种模式有两个优点，即加密/解密并行化，并且一个块中的噪声不会影响其他块。
 *
 * 6、GCM (Galois/Counter Mode)
 * 该模式是CTR模式的扩展。GCM受到了极大的关注，并被NIST推荐。GCM模型输出密文和身份验证标记。
 * 与算法的其他操作模式相比，该模式的主要优点是效率高。
 *
 *
 *  AES的块大小为128位或16字节。AES不改变大小，密文大小等于明文大小。此外，在ECB和CBC模式中，我们应该使用像PKCS 5这样的填充算法。所以加密后的数据大小为:
 * Ciphertext_size (bytes) = cleartext_size + (16 - (cleartext_size % 16))
 * 为了用密文存储IV，我们需要增加16个字节。
 *
 * secret key:
 * 在AES中生成密钥有两种方法:从随机数生成密钥，或从给定的密码生成密钥。
 * 在第一种方法中，密钥应该从像securerrandom类这样的加密安全(伪)随机数生成器生成。
 * 在第二种方法中，可以使用基于密码的密钥派生函数(如PBKDF2)从给定的密码派生AES密钥。我们还需要一个盐值来将密码转换为密钥。盐也是一个随机值。
 *
 * date: 2022/8/18  <br>
 * author: zss <br>
 * version: 1.0.0 <br>
 */
public class AESUtils {


    //为了生成密钥，我们可以使用KeyGenerator类。让我们定义一个生成n(128, 192和256)位AES密钥的方法:
    private static SecretKey generateKey(String key) {
        return new SecretKeySpec(Arrays.copyOf(DigestUtils.sha1(key),16), "AES");
    }
    //IV是一个伪随机值，与加密的块大小相同。
    private static IvParameterSpec generateIv(String key) {
        return new IvParameterSpec(Arrays.copyOf(DigestUtils.md5(key),16));
    }

    //我们可以使用带有PBKDF2WithHmacSHA256算法的SecretKeyFactory类从给定的密码生成密钥。
    //让我们定义一种从给定的密码生成AES密钥的方法，迭代次数为65,536次，密钥长度为256位:
    public static SecretKey generateKeyFromPassword(String password, String salt)
            throws NoSuchAlgorithmException, InvalidKeySpecException {

        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
        return secret;
    }


    private static String base64Encode(byte[] bytes){
        return Base64.getEncoder().encodeToString(bytes);
    }
    private static byte[] base64Decode(String text){
        return Base64.getDecoder().decode(text);
    }

    /**
     *
     * @param plainText
     * @param key 为16位字符
     * @return
     * @throws Exception
     */
    public static String encryptECB(String plainText, String key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, generateKey(key));
        byte[] result = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        return base64Encode(result);
    }

    public static String decryptECB(String cipherText, String key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, generateKey(key));
        byte[] result = cipher.doFinal(base64Decode(cipherText));
        return new String(result);
    }

    //string
    public static String encryptCBC(String plainText, String key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, generateKey(key), generateIv(key));
        byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
//        return Base64.encodeBase64String(cipherText);
        return base64Encode(cipherText);
    }
    public static String decryptCBC(String cipherText, String key) throws Exception{
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, generateKey(key), generateIv(key));
        byte[] plainText = cipher.doFinal(base64Decode(cipherText));
        return new String(plainText);
    }

    //string
    public static String encryptCTR(String plainText, String key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, generateKey(key), generateIv(key));
        byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        return base64Encode(cipherText);
    }
    public static String decryptCTR(String cipherText, String key) throws Exception{
        Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, generateKey(key), generateIv(key));
        byte[] plainText = cipher.doFinal(base64Decode(cipherText));
        return new String(plainText);
    }

    //file
    public static void encryptFile(File inputFile, File outputFile, String key) throws Exception {

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, generateKey(key), generateIv(key));
        FileInputStream inputStream = new FileInputStream(inputFile);
        FileOutputStream outputStream = new FileOutputStream(outputFile);
        byte[] buffer = new byte[64];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            byte[] output = cipher.update(buffer, 0, bytesRead);
            if (output != null) {
                outputStream.write(output);
            }
        }
        byte[] outputBytes = cipher.doFinal();
        if (outputBytes != null) {
            outputStream.write(outputBytes);
        }
        inputStream.close();
        outputStream.close();
    }
    public static void decryptFile(File encryptedFile, File decryptedFile, String key) throws Exception {

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE , generateKey(key), generateIv(key));
        FileInputStream inputStream = new FileInputStream(encryptedFile);
        FileOutputStream outputStream = new FileOutputStream(decryptedFile);
        byte[] buffer = new byte[64];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            byte[] output = cipher.update(buffer, 0, bytesRead);
            if (output != null) {
                outputStream.write(output);
            }
        }
        byte[] outputBytes = cipher.doFinal();
        if (outputBytes != null) {
            outputStream.write(outputBytes);
        }
        inputStream.close();
        outputStream.close();
    }


    //object
    public static SealedObject encryptObject(Serializable object,String key) throws Exception {

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, generateKey(key), generateIv(key));
        SealedObject sealedObject = new SealedObject(object, cipher);
        return sealedObject;
    }
    public static Serializable decryptObject(SealedObject sealedObject, String key) throws Exception {

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, generateKey(key), generateIv(key));
        Serializable unsealObject = (Serializable) sealedObject.getObject(cipher);
        return unsealObject;
    }
}
